vlwkaos' digital garden

GraphQL 실습 odyssey apollo

  • 데이터 모델을 어떻게 정의할까?

디자이너에게 화면 구성을 받았다. 필요한 데이터는?

  • 썸네일 이미지
  • 제목
  • 작성자
  • 작성자 아바타
  • 모듈 개수
  • 길이

Schema Definition Language (SDL)

GraphQL 스키마는 SDL을 이용하여 정의됨.

npm install apollo-server graphql

schema.js를 만들자

const typeDefs = gql`
"""
block comment like this!
"""
  type SpaceCat {
    "Line Comment like this"
    name: String!
    age: Int
    missions: [Mission]
  }

`

module.exports = typeDefs

backend

아까 설치했던 apollo-server를 사용하여 간단히 서빙: 서버의 역할은

  1. GraphQL 쿼리를 받는다.
  2. 받은 쿼리가 스키마와 맞는지 확인
  3. 스키마 필드에 맞는 데이터를 채워 반환
const {ApolloServer} = require('apollo-server');
const typeDefs = require('./schema');

const server = new ApolloServer({typeDefs});

server.listen().then(() => {
  console.log(`
    🚀  Server is running!
    🔉  Listening on port 4000
    📭  Query at https://studio.apollographql.com/dev
  `);
});

mocks 속성에 가짜 resolver를 넣을 수 있음. 아래 dev-studio 에 가보면 Query 엔트리가 기본적으로 입력되어있다.

https://studio.apollographql.com/

frontend

npm install graphql @apollo/client

아폴로 클라이언트 세팅

import {ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache()
});

필요한 페이지에서 gql 쿼리

import {gql} from '@apollo/client';

export const TRACKS = gql`
  query getTracks {
    tracksForHome {
      id
      title
      thumbnail
      length
      modulesCount
      author {
        name
        photo
      }
    }
  }
`;

Best Practices

  • Apollo Studio Explorer에서 테스트하고 복사하기
  • 각 query string을 ALL_CAPS 스타일로 변수 정의하기
  • 각 query를 export하여 unit test하기 쉽게 만들기

이런식으로 훅으로 사용가능

const Tracks = () => {
  const {loading, error, data} = useQuery(TRACKS); // 이건 @apollo/client꺼
  
  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;
  
  return <Layout grid>{JSON.stringify(data)}</Layout>;
};

query 결과를 감싸는 component를 만드는 것도 좋은 방법

어떻게 서버로 요청을 보내나?

GraphQL clients 쿼리 -> HTTP POST/GET + query string -> GraphQL server AST parse

resolver의 역할: 데이터 소스에서 가져온 데이터를 각 필드별로 어떤식으로 해결(데이터 반환) 할지 결정 데이터 소스는 다음이 될 수 있다.

  • 데이터베이스
  • 서드파티 API
  • webhook
  • ...

N+1 문제

Track마다 Author 정보를 가져와야 한다면? Track 배열을 가져오고 거기다 N번만큼 REST 요청을 엄청 많이 해야하나?

이처럼 단순히 fetch하는 방법으로는 제약이 많다.. 그래서 Cache를 잘 활용하고 RESTDataSource라는 것을 구현해서 이용한다.

RESTDataSource 구현하기

npm install apollo-datasource-rest
// src/datasources/track-api.js
const {RESTDataSource} = require('apollo-datasource-rest');
class TrackAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://odyssey-lift-off-rest-api.herokuapp.com/';
  }
  getTracksForHome() {
    return this.get('tracks');
  }
  getAuthor(authorId) {
    return this.get(`author/${authorId}`);
  }
}
module.exports = TrackAPI;

Resolver

인자 4가지: parent, args, context, and info

  • context 는 DB connection(datasource)정보, auth 등등 session...

사용하지 않는 인자는 컨벤션으로 _, __ 이런식으로 마킹한다.

Track의 Author 정보가 항상 필요하진 않다. 그래서 Track을 모두 가져오는 resolver에는 Author가져오는 걸 넣으면 안된다.

const resolvers = {
  Query: {
    tracksForHome: (_, __, {dataSources}) => {
      return dataSources.trackAPI.getTracksForHome();
    }
  },
  Track: {
    // 여기서 parent는 tracksForHome에서 받은 Track 정보를 갖는다.
    // 가져온 각 Track에 대해 Track resolver가 실행된다.
    author: (parent, _, {dataSources}) => {
    }
  }
}

module.exports = resolvers;

Best Practice

resolvers and data sources를 만들 때 resolver functions의 깊이를 최대한 얕게 하는게 변화/유지보수에 좋다. 그리고 읽기에도 편하고 리팩토링시에도 용이하다.

🤔 context의 dataSources는 어떻게 RESTDataSource알고있을까? 아직 그부분을 하지 않았다!

이 모든건 Apollo Server에서 연결된다.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => {
    return {
      trackAPI: new TrackAPI()
    };
  }
});

RESTDataSource가 어떻게 캐싱하는가?

  • 동시호출 막아줌
    • Promise전체를 memoize한다.
  • 캐싱 처리

Error...

Query에 인자 추가하여 받기

type Query {
  "Query to get tracks array for the homepage grid"
  tracksForHome: [Track!]!
  track(id: ID!): Track
}

각 트랙에서 modules를 가져올 때... track 의 resolver에서 가져와서 처리할 건가? 아니지...

Apollo Studio에서 schema 변경점 확인 가능...

Client쪽에서 변수 넘기기 아래처럼 할 수 있다.

const {loading, error, data} = useQuery(GET_TRACK, {variables: {trackId}});

mutation

type Mutation {
  addSpaceCat(name: String!): SpaceCat
}

만약 두 가지 오브젝트를 변형하는 mutation의 경우, 둘다 반환해준다. 그리고 code, success, message처럼 부가 정보고 가져온다.

type Mutation {
  incrementTrackViews(id: ID!): IncrementTrackViewsResponse
}

type IncrementTrackViewsResponse {
  code: Int!
  success: Boolean!
  message: String!
  track: Track
}

resolver 함수명은 schema 필드명과 같아야한다. 이런 경우 resolver에서 서버 응답에 따라 다른 값을 줘야하므로 비동기로 받은 후 result Object를 따로 생성해줘야한다.

쿼리 호출 방법

mutation IncrementTrackViewsMutation($incrementTrackViewsId: ID!) {
  incrementTrackViews(id: $incrementTrackViewsId) {
    code
    success
    message
    track {
      id
      numberOfViews
    }
  }
}

클라이언트에서 (웹프론트) useMutation을 이용한다.

const [incrementTrackViews, {loading, error, data}] = useMutation(INCREMENT_TRACK_VIEWS, { variables: {incrementTrackViewsId: id} });

프로덕션

Apollo Studio에서 Schema Registry 사용하기: Schema에 대한 Version Control을 제공 subgraph로 나뉘었을 때 충돌해결도 가능하다.

Apollo Studio에서 deployed graph로 만든 뒤, apollo server를 등록. 이러면 실행시마다 기록됨

APOLLO_KEY=service:xxxxx
APOLLO_GRAPH_ID=xxxxxx
APOLLO_GRAPH_VARIANT=current
APOLLO_SCHEMA_REPORTING=true

introspection을 공개하지 않고도, apollo studio를 통해 query를 실행해볼 수 있다.

서버, 클라이언트 따로 호스팅. 클라이언트에서 apollo server 설정할 때 미리 호스팅한 서버로 주소를 변경해줘야한다.

만약 더이상 특정 필드를 쓰지 못하게 된다면?

@deprecated schema directive로 명시해준다. 보통 schema directive는 reason 을 인자로 받는다.

"The track's approximate length to complete, in seconds"length: Int @deprecated(reason: "Use durationInSeconds")
GraphQL 실습 odyssey apollo